package a5.grammar.productions;

import java.util.LinkedList;
import java.util.List;

import a5.common.ProgramCursor;
import a5.common.OrderedTree;
import a5.common.Token;
import a5.environment.Environment;
import a5.function.Function;
import a5.grammar.annotations.Grammar;
import a5.grammar.grammars.G1;
import a5.grammar.interfaces.Production;

/**
 * <pre>
 * This class provides a method for interpreting the following production
 * Definition -> begin {VariableName} body Program end 
 * 
 * This production is composed of a "begin" token optionally followed by 
 * "VariableName" tokens representing argument variables for the function defined within the 
 * "body" and "end" tokens. 
 * 
 * This production uses recursive decent to interpret and parse.
 * </pre>
 * 
 * @author Jason Campos
 * @version Assignment 6, CS 152 (Programming Paradigms) Fall 2012, Jeffrey Smith
 */
@Grammar(clazz=G1.class)
public class Definition implements Production{
	// Production dependency
	private Program program;
	
	// Hide constructor
	private Definition(){}
	
	/**
	 * <pre>
	 * This method postpones (or ignores if never invoked) interpretation of the program contained within
	 * the "body" and (matching) "end" tokens by creating a {@link a5.function.Function Function} 
	 * which may be later {@link a5.function.Function#invoke(Environment, ProgramCursor, LinkedList) invoked}.
	 * </pre>
	 * 
	 * @param environment
	 * @param cursor
	 * @return
	 * @throws IllegalArgumentException
	 * Thrown when a "begin" token has no matching "end" token.
	 */
	public Function interpret(String functionName, Environment environment, ProgramCursor cursor) throws IllegalArgumentException{
		Function function = new DefinitionFunction(functionName, environment, cursor);
		
		cursor.match(environment, "begin");
		int startTokenCount = 1;
		
		// Just iterate over every token until end the matching end token
		// is found.  Increment startTokenCount each time a "begin" is found and 
		// decrement each time an "end" is found. When startTokenCount reaches 0,
		// the "end" token with corresponds to the first "begin" token is found.
		do{
			// If end of input is found, 
			if(cursor.first(Token.END_OF_INPUT_TYPE))
				throw new IllegalArgumentException("Unexpected end of input while parsing function");
			if(cursor.first("begin"))
				startTokenCount++;
			else if (cursor.first("end"))
				startTokenCount--;
			cursor.advance();
		} while (startTokenCount > 0);
	
		return function;
	}
	
	/**
	 * Recursively parses the program of the argument cursor using the variable/function scoping
	 * policies of the argument environment.
	 * 
	 * @param cursor
	 * The {@link a5.common.ProgramCursor ProgramCursor} which contains the {@link a5.common.Token Tokens} 
	 * of the program being interpreted.
	 * 
	 * @return
	 * An {@link a5.common.OrderedTree OrderedTree<Token>} representing the parse structure of the 
	 * the program of the argument cursor.
	 */
	public OrderedTree<Token> parse(ProgramCursor cursor){
		List<OrderedTree<Token>> children = new LinkedList<OrderedTree<Token>>();
		
		children.add(new OrderedTree<Token>(new Token(cursor.match("begin"))));
		
		// Continually peek at the next token's type. The VariableName
		// TokenType may be repeatedly indefinitely in this position.
		// Continue with the parse once a non 'VariableName' token type 
		// is in the queue.
		while(cursor.first("VariableName"))
			children.add(new OrderedTree<Token>(new Token(cursor.match("VariableName"))));
		
		children.add(new OrderedTree<Token>(new Token(cursor.match("body"))));
		children.add(program.parse(cursor));
		children.add(new OrderedTree<Token>(new Token(cursor.match("end"))));
		
		return new OrderedTree<Token>(new Token(this.getClass().getSimpleName()), children);
	}
	
	/**
	 * @param cursor
	 * The {@link a5.common.ProgramCursor ProgramCursor} which contains the {@link a5.common.Token Tokens} 
	 * of the program being interpreted.
	 * 
	 * @return
	 * {@code true} if the execution point of the program is at a point which indicates that
	 * an Expr is a valid path of execution. In other words, if the current point of
	 * execution contains an element which is the first element of this production 
	 * ("begin").
	 */
	public boolean first(ProgramCursor cursor) {
		return cursor.first("begin");
	}
	
	/**
	 * A {@link a5.function.Function Function} implementation for the Definition production. On instantiation, a 
	 * reference is created the the environment and program cursor to avoid the requirement of passing them
	 * in as arguments during invoke(). Also creates a reference to the beginning of the function definition 
	 * within the program cursor. This is used during invoke() to seek to the beginning of the function.
	 * 
	 * @author Jason Campos
	 */
	private class DefinitionFunction implements Function{
		/** Reference to execution environment */
		private Environment environment;
		/** Reference to program cursor */
		private final ProgramCursor cursor;
		/** The location of the first token of this Definition within the cursor */
		private final int location;
		/** The name for the function being defined */
		private String functionName;
		
		/**
		 * Creates a new {@link DefinitionFunction}
		 * @param functionName 
		 * {@link #functionName}
		 * @param environment
		 * {@link #environment}
		 * @param cursor 
		 * {@link #cursor}
		 */
		public DefinitionFunction(String functionName, Environment environment, ProgramCursor cursor){
			this.location = cursor.getLocation();
			this.cursor = cursor;
			this.environment = environment;
			this.functionName = functionName;
		}
		
		/*
		 * (non-Javadoc)
		 * @see a5.function.Function#invoke(java.util.LinkedList)
		 */
		@Override
		public String invoke(LinkedList<String> arguments) throws IllegalArgumentException {
			// Position of the cursor at run time of invoke()
			int returnTo = cursor.getLocation();
						
			try {
				// Begin a new environment frame. Give the frame the name of this function
				environment = environment.pushFrame(functionName);
				
				// location is the start position at runtime of Definition.interpret()
				cursor.seek(location);
				cursor.match(environment, "begin");
				// Go through each of the "VariableName" tokens for this function definition.
				// For each, store the spelling of the token in the parameters list. 
				// The end result is that the parameters collection contains the names of 
				// every parameter variable.
				LinkedList<String> parameters = new LinkedList<>();
				while(cursor.first("VariableName"))
					parameters.add(cursor.match(environment, "VariableName"));
				
				// If the number of function parameters found does not match the number of arguments
				// sent via invoke(), throw an exception indicating the arity mismatch
				if(parameters.size() != arguments.size())
					throw new IllegalArgumentException("arity mismatch. Expected: " + parameters.size() + ", Found: " + arguments.size());
				
				// If the number of arguments matched, bind the name/value pairs in the environment
				for(int i = 0; i < parameters.size(); i++)
					environment.setVariable(parameters.get(i), arguments.get(i));
				
				// Finally, perform the delayed interpretation
				cursor.match(environment, "body");
				String result = program.interpret(environment, cursor);
				cursor.match(environment, "end");
				
				// Return the result of the interpretation
				return result;
			} finally {
				// Return program execution to the point prior to invoke()
				// and pop the frame from the environment stack
				cursor.seek(returnTo);
				environment = environment.popFrame();
			}
		}
	}
}
